<?php

namespace Dreamcat\ConfigClient\Api\Impl;

use Dreamcat\ConfigClient\Api\NacosServerInterface;
use Dreamcat\ConfigClient\Consts\NacosConst;
use Dreamcat\ConfigClient\Nacos\ConfigDecodeInterface;
use Dreamcat\ConfigClient\Nacos\Impl\HtmlDecoder;
use Dreamcat\ConfigClient\Nacos\Impl\JsonDecoder;
use Dreamcat\ConfigClient\Nacos\Impl\PropertiesDecoder;
use Dreamcat\ConfigClient\Nacos\Impl\TextDecoder;
use Dreamcat\ConfigClient\Nacos\Impl\XmlDecoder;
use Dreamcat\ConfigClient\Nacos\Impl\YamlDecoer;
use Dreamcat\ConfigClient\Popo\Enum\NacosConfigType;
use Dreamcat\ConfigClient\Popo\NacosAppInfo;
use Dreamcat\ConfigClient\Popo\NacosAuthorization;
use Dreamcat\ConfigClient\Popo\NacosConfigResult;
use Dreamcat\ConfigClient\Popo\NacosNotifiParam;
use Dreamcat\ConfigClient\Popo\NacosTokenInfo;
use Dreamcat\Http\EnhanceClient\Share\RestHttpClient;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use RuntimeException;

/**
 * nacos配置中心
 * @author vijay
 */
class NacosServer implements NacosServerInterface
{
    /** @var string 服务器地址 */
    private $serverUrl;
    /** @var int 常规查询超时时长，ms */
    private $queryTimeoutMs = 500;
    /** @var int long pull的超时时长 */
    private $pullWaitTimeoutMs = 30000;
    /** @var RestHttpClient http客户端 */
    private $httpClient;
    /** @var LoggerInterface */
    private $logger;
    /** @var ConfigDecodeInterface[] 键是类型，值是解码器 */
    private $decoderMap;
    /** @var string 分隔符chr1 */
    private $chr1;
    /** @var string 分隔符chr2 */
    private $chr2;
    /** @var NacosTokenInfo 获取的token信息 */
    private $tokenInfo;
    /** @var NacosAuthorization nacos鉴权信息 */
    private $authorization;

    /**
     * NacosServer constructor.
     * @param string $serverUrl 服务器地址
     * @param array $decoderMap 键是类型，值是解码器
     */
    public function __construct(string $serverUrl, array $decoderMap = null)
    {
        $this->serverUrl = $serverUrl;
        $this->httpClient = new RestHttpClient();
        $this->logger = new NullLogger();
        $this->chr1 = chr(1);
        $this->chr2 = chr(2);
        $this->decoderMap = $decoderMap;
    }

    /**
     * 设置日志记录器
     * @param LoggerInterface $logger 日志记录器
     * @return static 当前对象
     */
    public function setLogger(LoggerInterface $logger): NacosServer
    {
        $this->logger = $logger;
        return $this;
    }

    /**
     * 设置解码器
     * @param NacosConfigType $nacosConfigType 解码的类型
     * @param ConfigDecodeInterface $decoder 解码器
     * @return static 当前对象
     */
    public function setDecoder(NacosConfigType $nacosConfigType, ?ConfigDecodeInterface $decoder): NacosServer
    {
        $this->decoderMap[$nacosConfigType->getValue()] = $decoder;
        return $this;
    }

    /**
     * @return string 服务器地址
     */
    public function getServerUrl(): string
    {
        return $this->serverUrl;
    }

    /**
     * @param string $serverUrl 服务器地址
     * @return static 对象本身
     */
    public function setServerUrl(string $serverUrl): NacosServer
    {
        $this->serverUrl = $serverUrl;
        return $this;
    }

    /**
     * @return int 常规查询超时时长
     */
    public function getQueryTimeoutMs(): int
    {
        return $this->queryTimeoutMs;
    }

    /**
     * @param int $queryTimeoutMs 常规查询超时时长
     * @return static 对象本身
     */
    public function setQueryTimeoutMs(int $queryTimeoutMs): NacosServer
    {
        if ($queryTimeoutMs >= 0) {
            $this->queryTimeoutMs = $queryTimeoutMs;
        }
        return $this;
    }

    /**
     * @return int long pull的超时时长
     */
    public function getPullWaitTimeoutMs(): int
    {
        return $this->pullWaitTimeoutMs;
    }

    /**
     * @param int $pullWaitTimeoutMs long pull的超时时长，ms，必须大于0
     * @return static 对象本身
     */
    public function setPullWaitTimeoutMs(int $pullWaitTimeoutMs): NacosServer
    {
        if ($pullWaitTimeoutMs > 0) {
            $this->pullWaitTimeoutMs = $pullWaitTimeoutMs;
        }
        return $this;
    }

    /**
     * @return NacosAuthorization
     */
    public function getAuthorization(): ?NacosAuthorization
    {
        return $this->authorization;
    }

    /**
     * @param NacosAuthorization $authorization
     * @return NacosServer
     */
    public function setAuthorization(?NacosAuthorization $authorization): NacosServer
    {
        $this->authorization = $authorization;
        $this->tokenInfo = null;
        return $this;
    }

    /**
     * @inheritDoc
     * @throws ClientExceptionInterface
     */
    public function getConfig(
        string $dataId,
        string $group = NacosConst::DEFAULT_GROUP,
        string $namespaceName = null
    ): NacosConfigResult {
        $url = "{$this->serverUrl}/nacos/v1/cs/configs";
        $params = [
            "dataId" => $dataId,
            "group" => $group,
        ];
        if ($namespaceName) {
            $params["tenant"] = $namespaceName;
        }
        $token = $this->getToken();
        if ($token) {
            $params["accessToken"] = $token;
        }
        $url .= "?" . http_build_query($params);
        $this->logger->info("getConfig url = {$url}");
        $request = $this->httpClient->getRequestFactory()->createRequest("GET", $url);
        $result = new NacosConfigResult();
        $result->setDataId($dataId)
            ->setGroup($group);
        if ($namespaceName) {
            $result->setNamespaceName($namespaceName);
        }
        try {
            $response = $this->httpClient->getHttpClient()->sendRequest($request, $this->queryTimeoutMs);
            switch ($response->getStatusCode()) {
                case 200:
                    $config = $response->getBody()->getContents();
                    $this->logger->info("nacos请求得到的http状态码 = {$response->getStatusCode()} config = {$config}");
                    $type = $response->getHeaderLine("Config-Type");
                    if (!NacosConfigType::isValid($type)) {
                        throw new RuntimeException("nacos提供的配置类型与预期的不一致");
                    }
                    $result->setSuccess(true)
                        ->setConfigurations($this->getDecoder(new NacosConfigType($type))->decode($config))
                        ->setReleaseKey(md5($config));
                    break;
                default:
                    $this->logger->warning("nacos获取配置的请求状态码 {$response->getStatusCode()}");
                    $result->setSuccess(false);
            }
        } catch (ClientExceptionInterface $e) {
            $this->logger->error("nacos请求出现异常 {$e->getMessage()}", [$e]);
            $result->setSuccess(false);
        }
        return $result;
    }

    /**
     * @inheritDoc
     * @throws ClientExceptionInterface
     */
    public function notifications(array $nacosNotifiParams): array
    {
        if (!$nacosNotifiParams) {
            return [];
        }
        $body = $this->buildPostData($nacosNotifiParams);
        $url = "{$this->serverUrl}/nacos/v1/cs/configs/listener";
        $token = $this->getToken();
        if ($token) {
            $url .= "?accessToken={$token}";
        }
        $this->logger->info("nacos 准备监听配置，url = {$url}, 请求参数 {$body}");
        $body = $this->httpClient
            ->getStreamFactory()
            ->createStream(http_build_query(["Listening-Configs" => $body]));
        $request = $this->httpClient
            ->getRequestFactory()
            ->createRequest("POST", $url);
        $request = $request
            ->withHeader("Long-Pulling-Timeout", strval($this->pullWaitTimeoutMs))
            ->withHeader("Content-Type", "application/x-www-form-urlencoded")
            ->withBody($body);

        try {
            $response = $this->httpClient->getHttpClient()->sendRequest($request);
            $statusCode = $response->getStatusCode();
            if ($statusCode == 200) {
                $responseBody = $response->getBody()->getContents();
                $this->logger->info("nacos 监控到配置变化，内容为 {$responseBody}");
                return $this->parseNotifiResult($responseBody);
            } else {
                $this->logger->warning("nacos 监控配置请求响应码为｛$statusCode} 请注意");
                return [];
            }
        } catch (ClientExceptionInterface $e) {
            $this->logger->error("nacos 监控配置出现异常 {$e->getMessage()}", [$e]);
            return [];
        }
    }

    /**
     * 获取解码器
     * @param NacosConfigType $nacosConfigType nacos配置类型
     * @return ConfigDecodeInterface 解码器
     */
    protected function getDecoder(NacosConfigType $nacosConfigType): ConfigDecodeInterface
    {
        $val = $nacosConfigType->getValue();
        if (isset($this->decoderMap[$val])) {
            return $this->decoderMap[$val];
        }
        switch ($val) {
            case NacosConfigType::TEXT:
                $decoder = new TextDecoder();
                break;
            case NacosConfigType::JSON:
                $decoder = new JsonDecoder();
                break;
            case NacosConfigType::XML:
                $decoder = new XmlDecoder();
                break;
            case NacosConfigType::YAML:
                $decoder = new YamlDecoer();
                break;
            case NacosConfigType::HTML:
                $decoder = new HtmlDecoder();
                break;
            default:
                $decoder = new PropertiesDecoder();
                break;
        }
        $this->decoderMap[$val] = $decoder;
        return $decoder;
    }

    /**
     * 构建监听配置的请求参数
     * @param NacosNotifiParam[] $nacosNotifiParams
     * @return string
     */
    private function buildPostData(array $nacosNotifiParams): string
    {
        $configs = "";
        foreach ($nacosNotifiParams as $nacosNotifiParam) {
            $configs .= $nacosNotifiParam->getDataId() . $this->chr2;
            $configs .= $nacosNotifiParam->getGroup() . $this->chr2;
            $configs .= $nacosNotifiParam->getReleaseKey() . $this->chr2;
            $namespance = $nacosNotifiParam->getNamespaceName();
            if ($namespance == NacosConst::DEFAULT_NAMESPACE) {
                $configs .= $this->chr1;
            } else {
                $configs .= "{$namespance}{$this->chr1}";
            }
        }
        return urlencode($configs);
    }

    /**
     * 解析监控结果
     * @param string $responseBody
     * @return NacosAppInfo[] 结果
     */
    private function parseNotifiResult(string $responseBody): array
    {
        $ary = explode($this->chr1, urldecode(trim($responseBody)));
        $list = [];
        foreach ($ary as $item) {
            $item = trim($item);
            if (!$item) {
                continue;
            }
            $tmp = explode($this->chr2, $item);
            $result = new NacosAppInfo();
            $result->setDataId($tmp[0])
                ->setGroup($tmp[1])
                ->setNamespaceName($tmp[2] ?? null);
            $list[] = $result;
        }
        return $list;
    }

    /**
     * @return string
     * @throws ClientExceptionInterface
     */
    private function getToken(): string
    {
        if (!$this->authorization) {
            return "";
        }
        if ($this->tokenInfo && $this->tokenInfo->getExpireTime() > time()) {
            return $this->tokenInfo->getToken();
        }
        $data = [
            "username" => $this->authorization->getUserName(),
            "password" => $this->authorization->getPassword(),
        ];
        $body = http_build_query($data);
        $url = "{$this->serverUrl}/nacos/v1/auth/login";
        $this->logger->info("nacos 尝试登录，url = {$url}, 请求参数 {$body}");
        $body = $this->httpClient->getStreamFactory()->createStream($body);
        $request = $this->httpClient->getRequestFactory()->createRequest("POST", $url);
        $request = $request
            ->withHeader("Content-Type", "application/x-www-form-urlencoded")
            ->withBody($body);
        $response = $this->httpClient->getHttpClient()->sendRequest($request);
        $statusCode = $response->getStatusCode();
        if ($statusCode != 200) {
            throw new RuntimeException("无法获取token信息");
        }
        $responseBody = $response->getBody()->getContents();
        $this->logger->info("nacos 成功获取token信息 {$responseBody}");
        $token = json_decode($responseBody, true);
        $this->tokenInfo = new NacosTokenInfo();
        $this->tokenInfo
            ->setToken($token["accessToken"])
            ->setExpireTime(time() + intval($token["tokenTtl"] * 0.9));
        return $this->tokenInfo->getToken();
    }
}

# end of file
